前幾天不管是 ReAct 還是 Plan-and-Execute,很多事情都是自己管的:
訊息紀錄
工具呼叫
執行迴圈
停止條件
Session 狀態
這樣手刻的好處是每一層都看得懂,但流程開始變多之後,程式也會越來越像一套自己做的 Agent Framework。
所以今天換個方向。
把部分流程交給 Google Agent Development Kit(ADK)。
不過有一件事先講清楚:
使用 Google ADK,不代表一定要使用 Google 的模型。
今天仍然使用前面一路沿用的:
Ollama
+
OllamaClient
只是把它接進 ADK 的模型介面。
這次先不碰 MCP。
先確認:
本機模型
↓
ADK Agent
↓
普通 Python Tool
↓
Session / Event
整條流程能正常跑通。
明天再把 devbench 接回來。
ADK 把我們前幾天自己處理的工作拆成幾個角色。
可以先這樣理解:
| 元件 | 負責什麼 |
|---|---|
| Agent | 定義模型、角色指示與可以使用的 Tools |
| Runner | 驅動一次 Agent 執行流程 |
| Session | 保存這段互動需要的狀態與事件 |
如果對照前面的手刻版本:
以前自己寫
messages
while / for loop
tool dispatch
state
↓
ADK
Agent
Runner
Session
不是突然多了一套新的 Agent 原理。
只是原本自己管理的東西,開始有固定介面可以使用。
今天先做一個非常簡單的 Tool:
def project_info() -> dict:
"""取得目前教學專案的基本資訊。"""
return {
"name": "devbench",
"purpose": "本機開發者工作台",
"language": "Python",
}
它不讀私人檔案,也不啟動任何子行程。
今天的目標不是展示 Tool 有多厲害,而是先確認 ADK 這一層真的接通。
如果現在就同時加入:
ADK
MCP
Ollama
Tool Policy
出錯時反而很難知道是哪一層有問題。
這次比較值得看的地方,是模型怎麼接進 ADK。
我們前面已經有自己的:
OllamaClient
沒必要為了換 Framework,就把整個模型層重寫。
所以新增一個 LocalModel,讓它實作 ADK 提供的 BaseLlm 介面。
概念上是:
ADK LlmRequest
↓
LocalModel
↓
轉成 ChatMessage
↓
OllamaClient.chat()
↓
本機 Ollama
模型回來後再反方向轉換:
Ollama Response
↓
LocalModel
↓
ADK LlmResponse
↓
Runner
最核心的方法是:
async def generate_content_async(...):
...
ADK 的 BaseLlm 把模型呼叫抽象成一層介面,所以 Framework 本身不需要知道底下到底是 Gemini、Ollama,還是其他推論服務。
Agent 就可以照常寫:
agent = Agent(
name="devbench",
model=LocalModel(),
instruction=SYSTEM,
tools=[project_info],
)
這裡我覺得最值得記的反而不是語法,而是這個分層:
Agent Framework
≠
Model Provider
Framework 管流程。
模型供應商負責推論。
兩邊可以分開替換。
雖然 ADK 本身支援的能力很多,但我們今天寫的 LocalModel 並沒有全部實作。
目前只處理:
文字
Tool / Function Calling
沒有實作:
圖片
音訊
Live API
Streaming
所以不能因為 ADK 支援這些能力,就寫成:
我們現在的本機 Agent 已經支援多模態與串流。
Framework 能做什麼,和目前 Adapter 實際做了什麼,是兩回事。
這跟前面 MCP 很像:
Server 提供四個 Tool
≠
Host 一定開放四個 Tool
能力要看實際接起來的那一層。
這次開發環境使用的 ADK 版本以專案裡的:
uv.lock
為準。
如果網路上的範例和本機 API 長得不一樣,第一件事不是一直改 Code,而是先確認:
目前到底裝哪個版本?
Agent Framework 更新速度很快,把不同版本的範例混在一起,通常只會多一堆很難解釋的錯誤。
今天的 main.py 很短:
import asyncio
import json
from ironman.adk_adapter import run_adk
async def main() -> None:
result = await run_adk()
print(
json.dumps(
result,
ensure_ascii=False,
indent=2,
)
)
assert result["answer"]
assert "project_info" in result["tool_calls"]
if __name__ == "__main__":
asyncio.run(main())
這次不只看:
模型最後有沒有回答
還會保存實際執行過的事件,例如:
Tool Call
模型呼叫次數
Session Event
最後答案
原因和前面幾天一樣。
假設模型最後回答:
我查過 devbench 的專案資訊。
不能只因為它說「我查過」就相信。
我們真正要確認的是事件裡有沒有:
project_info
所以測試要求:
"project_info" in result["tool_calls"]
真的成立。
模型自述不是執行紀錄。
隔離環境實測
前面手刻 Agent 時,我們自己設定:
max_steps
tool_budget
換成 ADK 之後,也不能因為 Framework 已經有執行機制,就完全不管限制。
這次仍然會限制模型最多可以被呼叫幾次。
因為對本機 GPU 來說:
一次 Agent 任務
如果因為錯誤流程變成:
模型
↓
Tool
↓
模型
↓
Tool
↓
模型
↓
Tool
↓
...
GPU 一樣會一直跑。
所以 Framework 可以幫我們管理 Loop。
但:
這個 Loop 最多允許跑多久?
仍然是應用程式自己的決策。
這跟 Day 13 的原則沒有變:
會自己繼續跑的流程,就一定要有上限。
今天使用的是:
InMemorySessionService
它可以保存目前這段執行需要的 Session 資料與事件。
但關鍵字在:
InMemory
程式一關掉,資料就沒了。
所以今天做到的是:
Session
不是:
長期記憶
更不是:
跨程式重啟恢復
資料庫持久化
跨裝置同步
這幾個概念不要因為都和「記住東西」有關,就混在一起。
目前可以先理解成:
Context
→ 這一輪模型現在看得到什麼
Session
→ 這段互動目前發生過什麼
Memory
→ 跨較長時間保留哪些有用資訊
Persistence
→ 程式重啟後資料還在不在
後面做到 OpenClaw 的 Session、Workspace 和 Memory 時,還會再回來拆這幾個概念。
到這裡就可以回頭跟前幾天比較。
原本自己寫 Agent:
建立 messages
↓
呼叫模型
↓
解析 Tool Call
↓
執行 Tool
↓
塞回 Observation
↓
判斷下一輪
今天改成:
建立 Agent
↓
交給 Runner
↓
Runner 管理模型與 Tool 的事件流程
↓
Session 保存狀態
少掉很多流程程式碼。
但有幾件事 ADK 沒有替我們決定:
Tool 到底安不安全
模型可以看到哪些能力
執行上限是多少
哪些資料可以讀
結果到底正不正確
這些問題不會因為換成 Framework 就消失。
Framework 解決的是:
怎麼把 Agent 流程組織得比較一致。
不是:
幫你自動做完所有 Agent 設計。
昨天的 Plan-and-Execute 還是:
我們的程式
↓
自己管理 Planner
↓
自己管理 Executor
↓
自己保存執行狀態
今天開始把部分責任交給 ADK:
ADK Agent
↓
Runner
↓
LocalModel
↓
Ollama
↓
Python Tool
而我們原本的 OllamaClient 還在。
代表:
換了 Agent Framework,不代表要把模型與前面的程式全部推倒重來。
今天只是先確認 Framework 與本機模型可以正常合作。
明天再補最後一塊:
ADK
↓
MCP
↓
devbench
看看 Day 10 寫好的 MCP Server,在換成另一套 Agent Framework 之後,是不是還能直接重用。
Day 18:
讓 Google ADK 使用 MCP:把既有工具接進新框架。
Google ADK Python - BaseLlm
https://github.com/google/adk-python/blob/main/src/google/adk/models/base_llm.py
Google Agent Development Kit
https://github.com/google/adk-python